/*
 * Copyright (C) 2015 Miquel Sas
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package traderlib.core.data.db;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import traderlib.core.data.db.sql.Select;
import traderlib.core.data.entity.Field;
import traderlib.core.data.entity.FieldList;
import traderlib.core.data.types.Types;
import traderlib.core.data.types.Value;

/**
 * A class to scan a view ofa database.
 *
 * @author Miquel Sas
 */
public class Cursor {

	/**
	 * Closed cursor error message.
	 */
	private final static String ERROR_CLOSED_CURSOR = "Cursor is closed";
	/**
	 * Forward only error message.
	 */
	private final static String ERROR_FORWAR_ONLY = "Unsupported operation for a forwar only cursor";

	/**
	 * The connection.
	 */
	private Connection cn;
	/**
	 * The prepared statement used to execute the query.
	 */
	private PreparedStatement ps;
	/**
	 * The result set to scan data.
	 */
	private ResultSet rs;
	/**
	 * The number of records per page.
	 */
	private int pageSize = 100;
	/**
	 * The column list.
	 */
	private FieldList columnList;
	/**
	 * The forward only flag.
	 */
	private boolean forwardOnly = true;
	/**
	 * A flag to control if this cursor is closed.
	 */
	private boolean closed = false;
	/**
	 * The last page read.
	 */
	private RecordSet page = null;
	/**
	 * The last record read.
	 */
	private Record record = null;

	/**
	 * Constructor assigning the connection, the select and indicating if the cursor shoulb forward only.
	 *
	 * @param cn The connection.
	 * @param select The select query.
	 * @param forwardOnly A boolean.
	 * @throws java.sql.SQLException
	 */
	public Cursor(Connection cn, Select select, boolean forwardOnly) throws SQLException {
		super();
		this.cn = cn;
		this.forwardOnly = forwardOnly;
		List<Value> values = select.getValues();
		String sql = select.toSQL();
		if (forwardOnly) {
			ps = cn.prepareStatement(sql);
		} else {
			ps = cn.prepareStatement(sql, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
		}
		for (int i = 0; i < values.size(); i++) {
			DBUtils.toPreparedStatement(values.get(i), i + 1, ps);
		}
		rs = ps.executeQuery();
		columnList = select.getView().getColumnList();
	}

	/**
	 * Constructs a forward only assigning the connection and the select.
	 *
	 * @param cn The connection.
	 * @param select The select query.
	 * @throws SQLException
	 */
	public Cursor(Connection cn, Select select) throws SQLException {
		this(cn, select, true);
	}

	/**
	 * Move before the first record. The last record and page read are left unmodified.
	 *
	 * @throws SQLException
	 */
	public void beforeFirst() throws SQLException {
		if (forwardOnly) {
			throw new SQLException(ERROR_FORWAR_ONLY);
		}
		if (closed) {
			throw new SQLException(ERROR_CLOSED_CURSOR);
		}
		rs.beforeFirst();
	}

	/**
	 * Move after the last record. The last record and page read are left unmodified.
	 *
	 * @throws SQLException
	 */
	public void afterLast() throws SQLException {
		if (forwardOnly) {
			throw new SQLException(ERROR_FORWAR_ONLY);
		}
		if (closed) {
			throw new SQLException(ERROR_CLOSED_CURSOR);
		}
		rs.afterLast();
	}

	/**
	 * Moves to the next page of this cursor and sets the last page read to be retrieved with getPage.
	 *
	 * @return <code>false</code> if past the last record, <code>true</code> otherwise.
	 * @throws SQLException
	 */
	public boolean nextPage() throws SQLException {
		if (closed) {
			throw new SQLException(ERROR_CLOSED_CURSOR);
		}
		RecordSet recordSet = new RecordSet();
		recordSet.setColumnList(columnList);
		int count = 0;
		while (count < pageSize && rs.next()) {
			count++;
			recordSet.add(readRecord());
		}
		page = recordSet;
		return (count == pageSize);
	}

	/**
	 * Moves to the next record of this cursor and sets the last record read to be retrieved with
	 * <code>getRecord</code>.
	 *
	 * @return <code>false</code> if past the last record, <code>true</code> otherwise.
	 * @throws SQLException
	 */
	public boolean nextRecord() throws SQLException {
		if (closed) {
			throw new SQLException(ERROR_CLOSED_CURSOR);
		}
		if (rs.next()) {
			record = readRecord();
			return true;
		}
		return false;
	}

	/**
	 * Moves to the first record of the second page and sets the last page read to be retrieved with
	 * <code>getPage</code>.
	 *
	 * @return <code>false</code> if no records present, <code>true</code> otherwise.
	 * @throws SQLException
	 */
	public boolean firstPage() throws SQLException {
		if (forwardOnly) {
			throw new SQLException(ERROR_FORWAR_ONLY);
		}
		if (closed) {
			throw new SQLException(ERROR_CLOSED_CURSOR);
		}
		RecordSet recordSet = new RecordSet();
		recordSet.setColumnList(columnList);
		rs.beforeFirst();
		int count = 0;
		while (count < pageSize && rs.next()) {
			count++;
			recordSet.add(readRecord());
		}
		page = recordSet;
		return (count == pageSize);
	}

	/**
	 * Moves to the first record of this cursor and sets the last record read to be retrieved with
	 * <code>getRecord</code>.
	 *
	 * @return <code>false</code> if no records present, <code>true</code> otherwise.
	 * @throws SQLException
	 */
	public boolean firstRecord() throws SQLException {
		if (forwardOnly) {
			throw new SQLException(ERROR_FORWAR_ONLY);
		}
		if (closed) {
			throw new SQLException(ERROR_CLOSED_CURSOR);
		}
		if (rs.first()) {
			record = readRecord();
			return true;
		}
		return false;
	}

	/**
	 * Moves to the record last page minus one of this cursor and sets the last page read to be retrieved with
	 * <code>getPage</code>.
	 *
	 * @return <code>false</code> if no records present, <code>true</code> otherwise.
	 * @throws SQLException
	 */
	public boolean lastPage() throws SQLException {
		if (forwardOnly) {
			throw new SQLException(ERROR_FORWAR_ONLY);
		}
		if (closed) {
			throw new SQLException(ERROR_CLOSED_CURSOR);
		}
		RecordSet recordSet = new RecordSet();
		recordSet.setColumnList(columnList);
		rs.afterLast();
		int count = 0;
		while (count < pageSize && rs.previous()) {
			count++;
			recordSet.add(readRecord());
		}
		page = recordSet;
		return (count == pageSize);
	}

	/**
	 * Moves to the last record of this cursor and sets the last record read to be retrieved with
	 * <code>getRecord</code>.
	 *
	 * @return <code>false</code> if no records present, <code>true</code> otherwise.
	 * @throws SQLException
	 */
	public boolean lastRecord() throws SQLException {
		if (forwardOnly) {
			throw new SQLException(ERROR_FORWAR_ONLY);
		}
		if (closed) {
			throw new SQLException(ERROR_CLOSED_CURSOR);
		}
		if (rs.last()) {
			record = readRecord();
			return true;
		}
		return false;
	}

	/**
	 * Moves to the previous page minus one of this cursor and sets the last page read to be retrieved with
	 * <code>getPage</code>.
	 *
	 * @return <code>false</code> if before the first record, <code>true</code> otherwise.
	 * @throws SQLException
	 */
	public boolean previousPage() throws SQLException {
		if (forwardOnly) {
			throw new SQLException(ERROR_FORWAR_ONLY);
		}
		if (closed) {
			throw new SQLException(ERROR_CLOSED_CURSOR);
		}
		RecordSet recordSet = new RecordSet();
		recordSet.setColumnList(columnList);
		int count = 0;
		while (count < pageSize && rs.previous()) {
			count++;
			recordSet.add(readRecord());
		}
		page = recordSet;
		return (count == pageSize);
	}

	/**
	 * Moves to the previous record of this cursor and sets the last record read to be retrieved with
	 * <code>getRecord</code>.
	 *
	 * @return <code>false</code> if before the first record, <code>true</code> otherwise.
	 * @throws SQLException
	 */
	public boolean previousRecord() throws SQLException {
		if (forwardOnly) {
			throw new SQLException(ERROR_FORWAR_ONLY);
		}
		if (closed) {
			throw new SQLException(ERROR_CLOSED_CURSOR);
		}
		if (rs.previous()) {
			record = readRecord();
			return true;
		}
		return false;
	}

	/**
	 * Returns all the records in this cursor as a <code>RecordSet</code>. The underlying resultset cursor is left open
	 * after the last record.
	 *
	 * @return All this cursor records in a <code>RecordSet</code>
	 * @throws SQLException
	 */
	public RecordSet getAllRecords() throws SQLException {
		return getAllRecords(0);
	}

	/**
	 * Returns all the records in this cursor as a <code>RecordSet</code>, up to a maximum number of records. The
	 * underlying result set cursor is left open after the last record.
	 *
	 * @param maxRecords The maximum number of records to retrieve, 0 or less means all.
	 * @return All this cursor records in a <code>RecordSet</code>
	 * @throws SQLException
	 */
	public RecordSet getAllRecords(int maxRecords) throws SQLException {
		if (closed) {
			throw new SQLException(ERROR_CLOSED_CURSOR);
		}
		RecordSet recordSet = new RecordSet();
		recordSet.setColumnList(columnList);
		int count = 0;
		while (rs.next()) {
			recordSet.add(readRecord());
			count++;
			if (maxRecords > 0 && count >= maxRecords) {
				break;
			}
		}
		return recordSet;
	}

	/**
	 * Returns all the records in this cursor as a <code>RecordSet</code> and close any used resource.
	 *
	 * @return All this cursor records in a <code>RecordSet</code>
	 * @throws SQLException
	 */
	public RecordSet getAllRecordsAndClose() throws SQLException {
		return getAllRecordsAndClose(0);
	}

	/**
	 * Returns all the records in this cursor as a <code>RecordSet</code> up to a maximum number of records and close
	 * any used resource.
	 *
	 * @param maxRecords The maximum number of records to retrieve, 0 or less means all.
	 * @return All this cursor records in a <code>RecordSet</code>
	 * @throws SQLException
	 */
	public RecordSet getAllRecordsAndClose(int maxRecords) throws SQLException {
		if (closed) {
			throw new SQLException(ERROR_CLOSED_CURSOR);
		}
		RecordSet recordSet = getAllRecords(maxRecords);
		close();
		return recordSet;
	}

	/**
	 * Returns the last record read.
	 * <p>
	 * @return The last record read.
	 */
	public Record getRecord() {
		return record;
	}

	/**
	 * Returns the last page read.
	 * <p>
	 * @return The last page read as a <code>RecordSet</code>
	 */
	public RecordSet getPage() {
		return page;
	}

	/**
	 * Get the number of records per page.
	 *
	 * @return The number of records per page.
	 */
	public int getPageSize() {
		return pageSize;
	}

	/**
	 * Set the number of records per page.
	 *
	 * @param pageSize The size of the pages read.
	 */
	public void setPageSize(int pageSize) {
		this.pageSize = pageSize;
	}

	/**
	 * Close this cursor and the underlying <code>ResultSet</code>, <code>PreparedStatement</code> and
	 * <code>Connection</code>.
	 *
	 * @throws SQLException
	 */
	public void close() throws SQLException {
		if (closed) {
			throw new SQLException(ERROR_CLOSED_CURSOR);
		}
		if (rs != null) {
			rs.close();
			rs = null;
		}
		if (ps != null) {
			ps.close();
			ps = null;
		}
		if (cn != null && !cn.isClosed()) {
			cn.close();
			cn = null;
		}
		closed = true;
	}

	/**
	 * Check if this cursor is closed.
	 *
	 * @return A boolean.
	 */
	public boolean isClosed() {
		return closed;
	}

	/**
	 * Let the garbage collector close this cursor if not already done.
	 */
	@Override
	protected void finalize() {
		try {
			close();
		} catch (SQLException e) {/* do nothing */

		}
	}

	/**
	 * Read the current record.
	 * <p>
	 * @return The current record.
	 * @throws SQLException
	 */
	private Record readRecord() throws SQLException {
		return DBUtils.readRecord(columnList, rs);
	}
}
